﻿////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// <copyright>Copyright 2008-2011 Andreas Huber Doenni</copyright>
// Distributed under the GNU General Public License version 2 (GPLv2).
// See accompanying file License.txt or copy at http://phuse.codeplex.com/license.
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

namespace Phuse.Net
{
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.IO;
    using System.Linq;
    using System.Text;

    using Phuse.IO;
    using Phuse.Text;

    /// <summary>Represents the base of all multiline responses.</summary>
    /// <typeparam name="TMostDerived">The most-derived subclass of this type.</typeparam>
    /// <typeparam name="TResponseLine">The type that exposes the data that is parsed from a single line of the
    /// response.</typeparam>
    /// <threadsafety static="true" instance="false"/>
    public abstract class MultilineResponseBase<TMostDerived, TResponseLine> :
        Response<TMostDerived>, IEnumerator<TResponseLine>
        where TMostDerived : MultilineResponseBase<TMostDerived, TResponseLine>
    {
        private Stream subsequentLines;
        private readonly Encoding encoding;
        private readonly ReadBuffer readBuffer;
        private readonly ArraySegment<byte>[] currentArguments;
        private readonly bool spaceDelimitsArguments;
        private readonly bool removeEmptyArguments;
        private TResponseLine currentLine;

        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////

        /// <summary>See <see cref="IEnumerator{T}.Current"/>.</summary>
        public TResponseLine Current
        {
            get { return this.currentLine; }
        }

        /// <summary>See <see cref="IDisposable.Dispose"/>.</summary>
        public void Dispose()
        {
            this.Dispose(true);
            GC.SuppressFinalize(this);
        }

        /// <summary>See <see cref="IEnumerator.MoveNext"/>.</summary>
        /// <exception cref="IOException">A general I/O error occurred, see <see cref="Exception.InnerException"/>
        /// for more information.</exception>
        /// <exception cref="ObjectDisposedException"><see cref="Dispose()"/> has been called, or, the next response has
        /// been received with <see cref="ClientBase{T}.GetResponse"/>.</exception>
        /// <exception cref="UnexpectedResponseException"><see cref="CreateResponseLine"/> threw
        /// <see cref="ArgumentException"/>, <see cref="FormatException"/> or <see cref="OverflowException"/>.
        /// </exception>
        /// <remarks>Moves to the next line of the response stream and splits it into an array of words. Then calls
        /// <see cref="CreateResponseLine"/> with the array and returns the result.</remarks>
        public bool MoveNext()
        {
            this.AssertNotDisposed();
            ArraySegment<byte> line = this.readBuffer.GetLine();

            if (line.Count == 0)
            {
                this.currentLine = default(TResponseLine);
                return false;
            }

            this.readBuffer.Index = EncodedBufferHelper.GetFirstLineWords(
                line, this.currentArguments, this.spaceDelimitsArguments, this.removeEmptyArguments);

            try
            {
                this.currentLine = this.CreateResponseLine(this.currentArguments);
                return true;
            }
            catch (Exception ex)
            {
                if (ex is ArgumentException || ex is FormatException || ex is OverflowException)
                {
                    var words = this.currentArguments.Where(s => s.Array != null).Select(
                        s => this.encoding.GetString(s.Array, s.Offset, s.Count));
                    throw new ExceptionHelper(words).CreateException<UnexpectedResponseException>(ex);
                }

                throw;
            }
        }

        /// <summary>See <see cref="IEnumerator.Reset"/>.</summary>
        /// <exception cref="NotSupportedException">Thrown with each call.</exception>
        public void Reset()
        {
            throw new NotSupportedException();
        }

        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////

        internal MultilineResponseBase(
            Stream subsequentLines,
            Encoding encoding,
            int argumentCount,
            bool spaceDelimitsArguments,
            bool removeEmptyArguments)
        {
            if (subsequentLines == null)
            {
                throw new ArgumentNullException("subsequentLines");
            }

            if (argumentCount <= 0)
            {
                throw new ArgumentOutOfRangeException("argumentCount", ExceptionMessages.PositiveNumberRequired);
            }

            this.subsequentLines = subsequentLines;
            this.encoding = encoding;

            // We're deliberately not using Settings.InMemoryStreamBufferSize because OverResponse regularily encounters
            // line lengths > the minimum of 1000 for that setting.
            this.readBuffer = new ReadBuffer(subsequentLines.Read, 4096);
            this.currentArguments = new ArraySegment<byte>[argumentCount];
            this.spaceDelimitsArguments = spaceDelimitsArguments;
            this.removeEmptyArguments = removeEmptyArguments;
        }

        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////

        /// <summary>When overridden in a derived class creates and returns a <typeparamref name="TResponseLine"/>
        /// object from <paramref name="responseLineWords"/>.</summary>
        /// <remarks>
        /// <para>It is assumed that overrides throw either <see cref="ArgumentException"/>,
        /// <see cref="FormatException"/> or <see cref="OverflowException"/> when they fail to parse any of the words in
        /// <paramref name="responseLineWords"/>, see <see cref="MoveNext"/> for more information.</para>
        /// <para><see cref="string.Length"/> of <paramref name="responseLineWords"/> is guaranteed to be equal to
        /// the argumentCount argument passed to the constructor.</para>
        /// </remarks>
        protected abstract TResponseLine CreateResponseLine(ArraySegment<byte>[] responseLineWords);

        /// <summary>Gets the encoding that is used to convert bytes into strings.</summary>
        protected Encoding Encoding
        {
            get { return this.encoding; }
        }

        /// <summary>Disposes the internally held <see cref="ResponseStream"/> object.</summary>
        protected virtual void Dispose(bool disposing)
        {
            if (disposing && !this.IsDisposed)
            {
                this.subsequentLines.Dispose();
                this.subsequentLines = null;
            }
        }

        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////

        private bool IsDisposed
        {
            get { return this.subsequentLines == null; }
        }

        private void AssertNotDisposed()
        {
            if (this.IsDisposed)
            {
                throw new ObjectDisposedException(this.ToString());
            }
        }

        /// <summary>See <see cref="IEnumerator.Current"/>.</summary>
        object IEnumerator.Current
        {
            get { return this.Current; }
        }
    }
}
